/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package axil.stdlib.collection.type;

import axil.api.AxilObject;
import axil.api.AxilType;
import axil.api.Formatting;
import axil.api.Persona;
import axil.framework.localization.Locality;
import axil.framework.localization.Translation;
import axil.framework.type.Added;
import axil.framework.type.Concatenated;
import axil.framework.type.Sized;
import axil.stdlib.core.type.Atom;
import axil.stdlib.core.type.Nil;
import axil.stdlib.numbers.type.Int;

import java.util.Iterator;

import static axil.framework.Functions.*;


/**
 * An object representing a range of integer values. The maximum of the range
 * can be infinite. This is a first-class Axil type, acting as a generator and
 * a collection. This is used extensively in Axil to provide for validation
 * checking on correctness in collections.
 */
public class Range extends Atom
	implements Sequence<Int>, Iterable<Int>, Sized, Keyed, Added, Concatenated {
	public static final AxilType type = Range_Type.object;
	public static final int infinity = -1;
	public static final Range
	optional = new Range(0, 1),
	required = new Range(1, 1),
	any = new Range(0, infinity),
	some = new Range(1, infinity);

	private final int min;
	private final int max;


	public Range(int min, int max) {
		if (min < 0) {
            throw error(axil(), "invalid-range-minimum", nv("min", min));
		}
		if ((max != infinity) && (max < min)) {
            throw error(axil(), "invalid-range-maximum",
                    nv("min", min), nv("max", max));
		}
		this.min = min;
		this.max = max;
	}


	public Range(int min) {
		if (min < 0) {
            throw error(axil(), "invalid-range-minimum", nv("min", min));
        }
		this.min = min;
		this.max = min;
	}


	public static Range from(String spec) {
		try {
			int i = spec.indexOf("..");
			int min;
			int max;
			if (i > 0) {
				min = Integer.parseInt(spec.substring(0, i));
				String m = spec.substring(i + 2);
				if (m.equals("*")) {
					max = infinity;
				} else {
					max = Integer.parseInt(m);
				}
			} else {
				min = Integer.parseInt(spec);
				max = min;
			}
			return new Range(min, max);
		} catch (NumberFormatException e) {
            throw error(axil(), "invalid-range-specification", nv("spec", spec));
		}
	}


	/**
	 * Get the type metadata for this object.
	 */
	public AxilType type() {
		return type;
	}


	/**
	 * Tell if this range indicates that only values greater than zero are
	 * within bounds. For example, a collection cannot be empty.
	 */
	public boolean some() {
		return min > 0;
	}


	public int min() {
		return min;
	}


	public int max() {
		return max;
	}


	private int min(int a, int b) {
		return a == infinity ? b : (b == infinity ? a : Math.min(a, b));
	}

	private int max(int a, int b) {
		return a == infinity ? b : (b == infinity ? b : Math.max(a, b));
	}


	/**
	 * Get the size of this object. The value is the number of contained objects,
	 * not the number of bytes consumed by the object. The size can be any value
	 * greater than or equal to zero.
	 */
	public int size() {
		if (max == infinity) {
			return 99999999;
		}
		if (max == 0) {
			return 0;
		}
		return max - min + 1;
	}


	/**
	 * Tell if the given value is within the bounds defined by this range.
	 */
	public boolean bounds(int size) {
		if (max == infinity) {
			return size >= min;
		}
		return (size >= min) && (size <= max);
	}


	public boolean bounds(java.util.Collection objects) {
		return bounds(objects.size());
	}


	public boolean bounds(AxilCollection objects) {
		return bounds(objects.size());
	}


	/**
	 * Return the key where the given object resides within this collection.
	 * A nil value is returned if the object does not live in this collection.
	 */
	public AxilObject keyof(AxilObject object) {
		int i = cast(Int.class, object).value();
		if (! bounds(i)) {
			return Nil.object;
		}
		return Int.from(i - min + 1);
	}


	/**
	 * Return the object at the given key, or nil if the key is not contained
	 * within this object.
	 */
	public AxilObject at(AxilObject key) {
		if (key == Nil.object) {
			return key;
		}
		int i = cast(Int.class, key).value();
		int v = min + i - 1;
		if (bounds(v)) {
			return Int.from(v);
		}
		return Nil.object;
	}


	/**
	 * Get the first element from this sequential collection, or return null
	 * if this collection is empty.
	 */
	public Int first() {
		return Int.from(min);
	}


	/**
	 * Get the last element from this sequential collection, or return null if
	 * this collection is empty.
	 */
	public Int last() {
		if (max == infinity) {
            throw error(axil(), "invalid-range-last");
		}
		return Int.from(max);
	}


	/**
	 * Get the element at the given index, where the first object is at index
	 * zero. If there is no element at that position, then a null is returned.
	 */
	public Int at(int index) {
		int value = min + index - 1;
		if (bounds(value)) {
			return Int.from(value);
		}
		return null;
	}


	/**
	 * Returns a hash code value for the object. All Axil values must provide a
	 * meaningful implementation of this function to enable themselves to be
	 * placed in a map or set.
	 */
	protected int hash() {
		return min + max;
	}


	/**
	 * Tell if this object is equivalent to the given object. The object given
	 * is never null. The object given may be of any type of value object.  If
	 * the given object is not a suitable type for comparison, a
	 * ClassCastException may be thrown.
	 */
	public boolean equalTo(AxilObject object) {
		if (object instanceof Range) {
			Range r = (Range)object;
			return (r.min == min) && (r.max == max);
		}
		return false;
	}


	/**
	 * Compares this object with the specified object for order. Ranges cannot
	 * be compared for order, since they do not have a symmetrical implementation
	 * where A > B implies B < A, since min/max can imply both > and < at the
	 * same time.
	 */
	public int comparedTo(AxilObject object) {
        throw error(axil(), "invalid-range-compare");
    }


	private static final class Iter implements Iterator<Int> {
		private Range range;
		private int i;

		private Iter(Range range) {
			if (range.max() == infinity) {
                throw error(axil(), "invalid-range-infinite",
                        nv("range", range));
			}
			this.range = range;
			this.i = range.min;
		}

		/**
		 * Returns true if the iteration has more elements. (In other words,
		 * returns true if next would return an element rather than throwing an
		 * exception.)
		 */
		public boolean hasNext() {
			return i <= range.max();
		}

		/**
		 * Returns the next element in the iteration.
		 */
		public Int next() {
			Int r = Int.from(i);
			i ++;
			return r;
		}


		/**
		 * Removes from the underlying collection the last element returned by the
		 * iterator. This is an unsupported operation.
		 */
		public void remove() {
            Persona persona = persona();
            Translation t = axil().translation("invalid-range-remove",
                    Locality.from(persona));
            String msg = t.text(persona);

            throw new UnsupportedOperationException(msg);
		}
	}


	/**
	 * Returns an iterator over a set of elements of this type.
	 */
	public Iterator<Int> iterator() {
		return new Iter(this);
	}


	/**
	 * Add the given value to this value, returning a new object. The given
	 * value is never null or nil. The value may be a numeric type of any other
	 * type of object that can sensibly implement the Added interface.
	 */
	public AxilObject add(AxilObject object) {
        if (object == Nil.object) {
            return Nil.object;
        }
        if (object instanceof Range) {
			Range other = (Range)object;
			return new Range(min(this.min, other.min), max(this.max, other.max));
		}
		throw error(axil(), "invalid-operand",
			  nv("operator", "+"), nv("type", object.type()));
	}


	/**
	 * Concatenate the given object onto this object, returning a new object.
	 * Neither this object nor the given object are altered. The given object
	 * is never null or nil, but may not be an object of the expected type.
	 */
	public AxilObject concatenate(AxilObject other) {
		return add(other);
	}


	/**
	 * Format this object in a locale-friendly manner. The string returned is
	 * a localized representation of this object using the locale and timezone
	 * associated with this persona. The formatting given is never null and
	 * matches formatting suitable for this type.
	 */
	public String format(Persona persona, Formatting formatting) {
		return toString();
	}


	/**
	 * Return a string representation of this object. The string returned is
	 * never null.
	 */
	public String toString() {
		if (max == infinity) {
			return min + "..*";
		}
		if (min == max) {
			return Integer.toString(min);
		}
		return min + ".." + max;
	}
}
